# -*- mode: python; coding: utf-8; -*-
##--------------------------------------------------------------------------##
##
## Copyright (C) 1998-2003 Markus Oberhumer <markus@oberhumer.com>
## Copyright (C) 2003 Mt. Hood Playing Card Co.
## Copyright (C) 2005-2010 Skomoroh <skomoroh@users.sourceforge.net>
## Copyright (C) 2010-2013 Jeremy Austin-Bardo <tjaustinbardo@gmail.com>
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, version 3 of the License.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##--------------------------------------------------------------------------##
import os
import imp
import logging

Logger = logging.getLogger('PySolFC.GameLib.GameDB')

from ..resourceng import ResourceManager, Resource


def _import(type_, path, name):
    #try:
    if type_ == 'sys':
        return __import__(path, globals(), locals(), [name])
    elif type_ == 'usr':
        return imp.load_source(name, path)
    #except (IOError, OSError, ImportError, NameError) as err:
    #    Logger.warning('Error loading game %s: %s', gs_mod, err)



class GameInfoException(Exception):
    """
    Game Info Exception class based on Exception(). Composes error message
    based on passed descriptive name and look up value.
    """

    def __init__(self, name='GAME_DB', value=None):
        """
        __init__(self, name='GAME_DB':str, value=None:str) => None

        name is the game info name passed to or used by GameInfo() for a game.
            - GIDR: Out of range or null game id.
            - GIDP: Protected usage for game id.
            - DECK: decks are greater than allowed.
            - NAME: name is duplicate or null.
        """
        value = {'GIDR': '%s: Game ID used is outside range or a null.',
                 'DECK': '%s: Cannot handle number of decks required.',
                 'NAME': '%s: Game Name is duplicated or a null.',
                 'GIDP': '%s: Game ID used is protected from future usage.'
                 }.get(value, '%s: General error has occured with Game.')
        Exception.__init__(self, value % name)


class GameInfo(Resource):
    """
    Game Info resource
    """

    def __init__(self, id, gameclass, name, game_type, decks, redeals=0, **kw):
        """
        Set a game info resource for a game object.

        GameInfo(self, id:int, gameclass:class, name:str, game_type:int,
                 decks:int[, redeals:int=0, skill_level:int=None, si:dict={},
                 category:int=GC.FRENCH, short_name:str=name,
                 altnames:seq(str ...)=[], suits:seq(int ...)=[0, 1, 2, 3],
                 ranks:seq(int ...)=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12),
                 trumps:seq(int ...)=[], bonus:seq(int ...)=[],
                 rules_filename:str=None, version:str='0']) => None
        """
        import re
        from .info import GC, category_convert
        from ..settings import TRANSLATE_GAME_NAMES
        from ..packlib import group as CSG

        def encode(name):
            """
            encode name as UTF-8 and alternatively translate name with GetText.
            """
            if not isinstance(name, unicode):
                name = unicode(name, 'utf-8', 'ignore')
            return _(name) if TRANSLATE_GAME_NAMES else name

        for k, v in [('skill_level', None), ('si', {}),
                     ('altnames', ()), ('short_name', name),
                     ('suits', range(4)), ('ranks', range(13)), ('trumps', ()),
                     ('bonus', []), ('rules_filename', None), ('version', 0)]:
            kw.setdefault(k, v)

        if not (1 <= id <= 999999):
            raise GameInfoException('{} of {}'.format(name, id), 'GIDR')
        kw['id'] = id
        kw['gameclass'] = gameclass.__module__, gameclass.__name__
        kw['decks'] = decks
        kw['redeals'] = redeals
        kw['ncards'] = decks * (
                        len(kw['suits']) * len(kw['ranks']) + len(kw['trumps']))
        # Accomodate cardsets like mahjongg...
        kw['ncards'] += len(kw['bonus']) * kw['si'].get('decks', 0)
        if not name:
            raise GameInfoException(name, 'GAME')
        kw['name'] = encode(name)
        kw['short_name'] = encode(kw['short_name'][:20])

        if isinstance(kw['altnames'], basestring):
            kw['altnames'] = (kw['altnames'], )
        kw['altnames'] = [encode(alt_name) for alt_name in kw['altnames']]
        # set category of pack type
        game_type = game_type & 1023
        if not (kw.get('category') in CSG.TYPE):
            kw['category'] = category_convert(game_type)
        if kw['category'] == GC.MAHJONGG and decks % 4:
            raise GameInfoException('{} for {}'.format(name, decks), 'DECK')
        elif kw['category'] != GC.MAHJONGG and not 1 <= decks <= 4:
            raise GameInfoException('{} for {}'.format(name, decks), 'DECK')

        kw['si'].update({
                        'game_flags': game_type & ~1023, 'game_type': game_type,
                        'decks': decks, 'redeals': redeals,
                        'ncards': kw['ncards'], 'version': kw['version'],
                        'credit': tuple(gameclass.__credits__)
                                    if hasattr(gameclass, '__credits__')
                                    else tuple()})

        if not kw['rules_filename']:
            kw['rules_filename'] = (re.sub(r'[^\w]', '', name).lower())

        kw['save_filename'] = (re.sub(r'[^\w]', '',
                                   re.sub(r'[^\s]', '_', name)).lower())
        kw['class_filename'] = None

        super(GameInfo, self).__init__(**kw)

    def __call__(self):
        """
        Import and intialize game class associated with game info.

        .. GameInfo() -> obj
        """
        if self['class_filename'] is None:
            module = _import('sys', path=self['gameclass'][0],
                                        name=[self['gameclass'][1]])
        else:
            module = _import('user', path=self['class_filename'],
                                         name=[self['gameclass'][1]])
        return module.__dict__[self['gameclass'][1]](self)

    def __repr__(self):
        """
        Present basic details of game info.

        .. GameInfo -> str
        """
        from ..packlib import group as CSG
        id_, name, category = '###', 'unknown name', 'unknown category'
        if 'short_name' in self.__dict__:
            name = self['short_name']
        if 'id' in self.__dict__:
            id_ = self['id']
        if 'category' in self.__dict__:
            category = CSG.TYPE.get(self['category'], category)
        return '<GameInfo Resource: [id: {}, name: {}, category: {}]>'.format(
                                                            id_, name, category)


class GameManager(ResourceManager):
    """
    Game Manager to manage game info resources
    """
    registered_game_types = {}
    __callback = None            # update progress-bar (see __init__.py)

    def __init__(self):
        """
        Augment to include Game specific cache and sorted keys.

        .. \__init__() -> None
        """
        super(GameManager, self).__init__()
        self._cache.update({'id': {}})
        self._sorted.update({'id': [], 'short_name': [], 'alt_name': []})
    def set_callback(self, func):
        """
        Set progress callback function.

        .. set_callback(self, func:def)
        """
        self.__callback = func

    def __check_gameinfo(self, gameinfo):
        """
        __check_gameinfo(self, gameinfo:Resource) => bool

        Success or failure of a game info comparison to all game info available.
        """
        # print 'check game:', gameinfo['id'], gameinfo['short_name']
        err_msg = []
        for obj in self:
            if gameinfo['id'] == obj['id']:
                err_msg.append('id: {} and {}'.format(repr(gameinfo),
                                                      repr(obj['gameclass'])))
            if gameinfo['name'] == obj['name']:
                err_msg.append('name: {} and {}'.format(repr(gameinfo),
                                                        repr(obj['gameclass'])))
            if gameinfo['gameclass'] is obj['gameclass']:
                err_msg.append('class: {} and {}'.format(repr(gameinfo),
                                                        repr(obj['gameclass'])))
            for name in gameinfo['altnames']:
                if name in obj['altnames']:
                    err_msg.append('alt name [{}]: {} and {}'.format(name,
                                                        repr(gameinfo),
                                                        repr(obj['gameclass'])))
        err_msg = '\n'.join('Duplicate game {}'.format(m) for m in err_msg)
        logging.warning(err_msg)
        return bool(err_msg)

    def register(self, resource):
        """
        Register game resource detail.

        Overide to update given resource.

        .. register(self, resource:GameInfo) -> None

        *Parameters:*
          ``resource``: {mapping}
            Details of a game to be added.
        """
        from .info import GT
        from ..settings import CHECK_GAMES
        try:
            # Confirm resource is GameInfo.
            if not isinstance(resource, GameInfo):
                raise GameInfoException('Register Resource', 'CLASS')
            # Check for duplications.
            if CHECK_GAMES and self.__check_gameinfo(resource):
                return
            cid = len(self)
            super(GameManager, self).register(resource)
            # Add current id to name and id. Include alt names in name.
            if not (resource['si']['game_flags'] & GT.HIDDEN):
                self._cache['id'][resource['id']] = cid
                self._cache['name'][resource['name']] = cid
                for n in resource['altnames']:
                    self._cache['name'][n] = cid
            if self.__callback and cid % 10 == 0:
                self.__callback(step=1)
        except GameInfoException as err:
            Logger.warning('Error registering game %s: %s',
                            info.get('name', 'unkown'), err)

    def load(self, func, plugins):
        """
        Get game details from system and user path.

        The given func will filter which game category library is stored from
        the system path. The given plugins will locate game modules. Each will
        call load games methods of load library/module to yeild game details.

        .. load(self, gs:int, gs_all:bool) => None
        """
        for gs_id, gs_mod in ((1, ''), (2, '.tarock'), (4, '.dashavatara'),
                           (8, '.mughal'), (16, '.hanafuda'), (32, '.hexadeck'),
                           (64, '.mahjongg'), (128, '.puzzle'),
                           (256, '.shisensho'), (512, '.special'), ):
            if func(gs_id):
                module = _import('sys', path='pysollib.games{}'.format(gs_mod),
                                        name='loadgames')
                for info in module.__dict__['loadgames']():
                    self.register(GameInfo(**info))

        if plugins:
            plugins.sort()
            for name in plugins:
                game_mod = _import('usr', name[:-3], name)
                for info in getattr(module, 'loadgames', list)():
                    self.register(GameInfo(**info))
                    self[-1]['class_filename'] = name

    def _do_sorting(self):
        """
        _do_sorting(self) => None

        Sorting hook called by base Resource manager on first call to sorted()
        """
        # invalidate sorted lists
        self._sorted = {'id': [], 'name': [], 'short_name': [],
                        'alt_name': []}

        gameids = self._cache['id'].keys()
        gameids.sort()
        self._sorted['id'] = [self._cache['id'][gi] for gi in gameids]
        f_name, s_name, a_name = [], [], []
        for name, gameinfo in ((obj['name'], obj) for obj in self):
            id_ = self._cache['name'][name]
            f_name.append((name, id_))
            if name != gameinfo['short_name']:
                name = gameinfo['short_name']
            s_name.append((name, id_))
            a_name.extend([(altname, id_) for altname in gameinfo['altnames']])
        f_name.sort()
        s_name.sort()
        a_name.sort()
        # Store ids of games not actual names
        self._sorted['name'] = tuple([i[1] for i in f_name])
        self._sorted['short_name'] = tuple([i[1] for i in s_name])
        self._sorted['alt_name'] = tuple([i[1] for i in a_name])


# the global game database (the single instance of class GameManager)
GAME_DB = GameManager()


def prepare(args, progress=None):
    """
    Prepare games database with system and user game mods.

    .. \prepare(args:OptArg, progress:Progress=None) -> int|None

    *Parameters:*
      args: {OptionArgs}
        Arguments read from the command line
      progress: {Progress}
    """
    from ..utillib.pathdb import PATH_DB, NJ_PATH
    from ..settings import CONF_OPT
    plugins = []
    if not args.noplugins:
        usrpath = PATH_DB.dn['plugins']
        plugins = [NJ_PATH(usrpath, n) for n in (p for p in os.listdir(usrpath))
                                if n[-3:] == '.py' and n[0] != '_'
                                    and os.path.isfile(NJ_PATH(usrpath, n))]
    if progress:
        GAME_DB.set_callback(progress.update)
    GAME_DB.load(func=lambda i, g=args.gs, a=args.gs_all: i in g or a,
                 plugins=plugins)
    GAME_DB.set_callback(None)
    if len(GAME_DB) == 0:
        return
    # Set last game played by id.
    gid = GAME_DB.find(args.gn, 'name')
    if gid is None and args.gn:
        logging.warning('Cannot find game: %s' % args.gn)
    if progress:
        progress.update(step=1)
    gid = args.gi or gid or GAME_DB.find(CONF_OPT['last_gameid'])
    if 0 <= gid < len(GAME_DB):
        return gid
    else:
        logging.warning('Cannot find game: %s' % gid)
    if progress:
        progress.update(step=1)